package com.wewins.fota.sys.token.impl;

import com.wewins.fota.common.constant.AuthConst;
import com.wewins.fota.sys.token.TokenManager;
import io.jsonwebtoken.*;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
@Data
@Slf4j
public class JwtTokenManager implements TokenManager {
    @Value("${fota.token.expire:30m}")
    private Duration tokenExpiration;
    @Value("${fota.token.salt:fota-manager-auto-salt}")
    private String tokenSignKey;
    @Value("${fota.token.refresh-time:5m}")
    private Duration refreshTime;
    @Value("${fota.token.random-salt:true}")
    private boolean randomSalt;

    private final SecureRandom random = new SecureRandom();


    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    public JwtTokenManager(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @PostConstruct
    public void initMethod() {
        // warm up data
        this.generateJwtToken("-1");
    }

    /**
     * random salt:
     *
     * @see BCryptPasswordEncoder#encode(CharSequence)
     */
    @Override
    public String createToken(String username) {
        return this.updateToken(username, null);
    }

    private String updateToken(String username, String token) {
        log.info("start to generate or update jwt token.");
        if (StringUtils.isEmpty(token)) {
            token = UUID.randomUUID().toString().replace("-", "");
        }
        this.calcSignKey(token);
        String tokenDetail = generateJwtToken(username);
        redisTemplate.opsForValue().set(this.getInnerTokenKey(token), tokenDetail, 2 * tokenExpiration.toMillis(), TimeUnit.MILLISECONDS);
        redisTemplate.opsForValue().set(this.getSignRedisKey(token), this.tokenSignKey, 2 * tokenExpiration.toMillis(), TimeUnit.MILLISECONDS);
        log.info("end to generate or update jwt token.");
        return token;
    }

    private void calcSignKey(String token) {
        if (!randomSalt) {
            return;
        }
        this.tokenSignKey = redisTemplate.opsForValue().get(this.getSignRedisKey(token));
        if (StringUtils.isEmpty(this.tokenSignKey)) {
            this.tokenSignKey = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, random);
        }
    }

    private String generateJwtToken(String username) {
        return Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration.toMillis()))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
    }

    private String getInnerTokenKey(String token) {
        return String.format(AuthConst.REDIS_HASH_AUTH_TOKEN_VALUE, token);
    }

    private String getSignRedisKey(String outToken) {
        return String.format(AuthConst.REDIS_HASH_AUTH_TOKEN_SIGN, outToken);
    }

    @Override
    public String getUsernameFromToken(String outToken) {
        return getBody(outToken).getSubject();
    }

    @Override
    public void removeToken(String outToken) {
        redisTemplate.delete(AuthConst.REDIS_HASH_AUTH_TOKEN);
    }

    @Override
    public String refreshToken(String outToken) {
        Claims claims = this.getBody(outToken);
        Date expiration = claims.getExpiration();
        if (expiration.compareTo(new Date(System.currentTimeMillis() + tokenExpiration.toMillis() - refreshTime.toMillis())) < 0) {
            String username = claims.getSubject();
            return this.updateToken(username, outToken);
        }
        return outToken;
    }


    private Claims getBody(String outToken) {
        String innerToken = redisTemplate.opsForValue().get(this.getInnerTokenKey(outToken));
        this.calcSignKey(outToken);
        if (StringUtils.isEmpty(innerToken)) {
            throw new ExpiredJwtException(null, null, "jwt expired in redis, redis expire time is twice jwt expire time.");
        }
        try {
            return Jwts.parser().setSigningKey(this.tokenSignKey).parseClaimsJws(innerToken).getBody();
        } catch (ExpiredJwtException | SignatureException e) {
            this.removeToken(outToken);
            throw e;
        }
    }
}
